/* globals CryptoJS, blowfish */

/**
 * Cipher operations.
 *
 * @author n1474335 [n1474335@gmail.com]
 * @copyright Crown Copyright 2016
 * @license Apache-2.0
 *
 * @namespace
 */
var Cipher = {
    
    /**
     * @constant
     * @default
     */
    IO_FORMAT1: ["Hex", "Base64", "UTF8", "UTF16", "UTF16LE", "UTF16BE", "Latin1"],
    /**
     * @constant
     * @default
     */
    IO_FORMAT2: ["UTF8", "UTF16", "UTF16LE", "UTF16BE", "Latin1", "Hex", "Base64"],
    /**
     * @constant
     * @default
     */
    IO_FORMAT3: ["Hex", "Base64", "UTF16", "UTF16LE", "UTF16BE", "Latin1"],
    /**
     * @constant
     * @default
     */
    IO_FORMAT4: ["Latin1", "UTF8", "UTF16", "UTF16LE", "UTF16BE", "Hex", "Base64"],
    /**
     * @constant
     * @default
     */
    MODES: ["CBC", "CFB", "CTR", "OFB", "ECB"],
    /**
     * @constant
     * @default
     */
    PADDING: ["Pkcs7", "Iso97971", "AnsiX923", "Iso10126", "ZeroPadding", "NoPadding"],
    /**
     * @constant
     * @default
     */
    RESULT_TYPE: ["Show all", "Ciphertext", "Key", "IV", "Salt"],
    
    
    /**
     * Runs encryption operations using the CryptoJS framework.
     *
     * @private
     * @param {function} algo - The CryptoJS algorithm to use
     * @param {byte_array} input
     * @param {function} args
     * @returns {string}
     */
    _enc: function (algo, input, args) {
        var key = Utils.format[args[0].option].parse(args[0].string || ""),
            iv = Utils.format[args[1].option].parse(args[1].string || ""),
            salt = Utils.format[args[2].option].parse(args[2].string || ""),
            mode = CryptoJS.mode[args[3]],
            padding = CryptoJS.pad[args[4]],
            result_option = args[5].toLowerCase(),
            output_format = args[6];
        
        if (iv.sigBytes === 0) {
            // Use passphrase rather than key. Need to convert it to a string.
            key = key.toString(CryptoJS.enc.Latin1);
        }
        
        var encrypted = algo.encrypt(input, key, {
            salt: salt.sigBytes > 0 ? salt : false,
            iv: iv.sigBytes > 0 ? iv : null,
            mode: mode,
            padding: padding
        });
        
        var result = "";
        if (result_option == "show all") {
            result += "Key:  " + encrypted.key.toString(Utils.format[output_format]);
            result += "\nIV:   " + encrypted.iv.toString(Utils.format[output_format]);
            if (encrypted.salt) result += "\nSalt: " + encrypted.salt.toString(Utils.format[output_format]);
            result += "\n\nCiphertext: " + encrypted.ciphertext.toString(Utils.format[output_format]);
        } else {
            result = encrypted[result_option].toString(Utils.format[output_format]);
        }
        
        return result;
    },
    
    
    /**
     * Runs decryption operations using the CryptoJS framework.
     *
     * @private
     * @param {function} algo - The CryptoJS algorithm to use
     * @param {byte_array} input
     * @param {function} args
     * @returns {string}
     */
    _dec: function (algo, input, args) {
        var key = Utils.format[args[0].option].parse(args[0].string || ""),
            iv = Utils.format[args[1].option].parse(args[1].string || ""),
            salt = Utils.format[args[2].option].parse(args[2].string || ""),
            mode = CryptoJS.mode[args[3]],
            padding = CryptoJS.pad[args[4]],
            input_format = args[5],
            output_format = args[6];
        
        // The ZeroPadding option causes a crash when the input length is 0
        if (!input.length) {
            return "No input";
        } 
        
        var ciphertext = Utils.format[input_format].parse(input);
        
        if (iv.sigBytes === 0) {
            // Use passphrase rather than key. Need to convert it to a string.
            key = key.toString(CryptoJS.enc.Latin1);
        }
        
        var decrypted = algo.decrypt({
                ciphertext: ciphertext,
                salt: salt.sigBytes > 0 ? salt : false
            }, key, {
                iv: iv.sigBytes > 0 ? iv : null,
                mode: mode,
                padding: padding
            });
        
        var result;
        try {
            result = decrypted.toString(Utils.format[output_format]);
        } catch (err) {
            result = "Decrypt error: " + err.message;
        }
        
        return result;
    },
    
    
    /**
     * AES Encrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_aes_enc: function (input, args) {
        return Cipher._enc(CryptoJS.AES, input, args);
    },
    
    
    /**
     * AES Decrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_aes_dec: function (input, args) {
        return Cipher._dec(CryptoJS.AES, input, args);
    },
    
    
    /**
     * DES Encrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_des_enc: function (input, args) {
        return Cipher._enc(CryptoJS.DES, input, args);
    },
    
    
    /**
     * DES Decrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_des_dec: function (input, args) {
        return Cipher._dec(CryptoJS.DES, input, args);
    },
    
    
    /**
     * Triple DES Encrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_triple_des_enc: function (input, args) {
        return Cipher._enc(CryptoJS.TripleDES, input, args);
    },
    
    
    /**
     * Triple DES Decrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_triple_des_dec: function (input, args) {
        return Cipher._dec(CryptoJS.TripleDES, input, args);
    },
    
    
    /**
     * Rabbit Encrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_rabbit_enc: function (input, args) {
        return Cipher._enc(CryptoJS.Rabbit, input, args);
    },
    
    
    /**
     * Rabbit Decrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_rabbit_dec: function (input, args) {
        return Cipher._dec(CryptoJS.Rabbit, input, args);
    },
    
    
    /**
     * @constant
     * @default
     */
    BLOWFISH_MODES: ["ECB", "CBC", "PCBC", "CFB", "OFB", "CTR"],
    /**
     * @constant
     * @default
     */
    BLOWFISH_OUTPUT_TYPES: ["Base64", "Hex", "String", "Raw"],
    
    /**
     * Blowfish Encrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_blowfish_enc: function (input, args) {
        var key = Utils.format[args[0].option].parse(args[0].string).toString(Utils.format.Latin1),
            mode = args[1],
            output_format = args[2];
            
        if (key.length === 0) return "Enter a key";
        
        var enc_hex = blowfish.encrypt(input, key, {
                outputType: 1,
                cipherMode: Cipher.BLOWFISH_MODES.indexOf(mode)
            }),
            enc = CryptoJS.enc.Hex.parse(enc_hex);
            
        return enc.toString(Utils.format[output_format]);
    },
    
    
    /**
     * Blowfish Decrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_blowfish_dec: function (input, args) {
        var key = Utils.format[args[0].option].parse(args[0].string).toString(Utils.format.Latin1),
            mode = args[1],
            input_format = args[2];
            
        if (key.length === 0) return "Enter a key";
        
        input = Utils.format[input_format].parse(input);
        
        return blowfish.decrypt(input.toString(CryptoJS.enc.Base64), key, {
            outputType: 0, // This actually means inputType. The library is weird.
            cipherMode: Cipher.BLOWFISH_MODES.indexOf(mode)
        });
    },
    
    
    /**
     * @constant
     * @default
     */
    KDF_KEY_SIZE: 256,
    /**
     * @constant
     * @default
     */
    KDF_ITERATIONS: 1,
    
    /**
     * Derive PBKDF2 key operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_pbkdf2: function (input, args) {
        var key_size = args[0] / 32,
            iterations = args[1],
            salt = CryptoJS.enc.Hex.parse(args[2] || ""),
            input_format = args[3],
            output_format = args[4],
            passphrase = Utils.format[input_format].parse(input),
            key = CryptoJS.PBKDF2(passphrase, salt, { keySize: key_size, iterations: iterations });
        
        return key.toString(Utils.format[output_format]);
    },
    
    
    /**
     * Derive EVP key operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_evpkdf: function (input, args) {
        var key_size = args[0] / 32,
            iterations = args[1],
            salt = CryptoJS.enc.Hex.parse(args[2] || ""),
            input_format = args[3],
            output_format = args[4],
            passphrase = Utils.format[input_format].parse(input),
            key = CryptoJS.EvpKDF(passphrase, salt, { keySize: key_size, iterations: iterations });
        
        return key.toString(Utils.format[output_format]);
    },
    
    
    /**
     * RC4 operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_rc4: function (input, args) {
        var message = Utils.format[args[1]].parse(input),
            passphrase = Utils.format[args[0].option].parse(args[0].string),
            encrypted = CryptoJS.RC4.encrypt(message, passphrase);
            
        return encrypted.ciphertext.toString(Utils.format[args[2]]);
    },
    
    
    /**
     * @constant
     * @default
     */
    RC4DROP_BYTES: 768,
    
    /**
     * RC4 Drop operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run_rc4drop: function (input, args) {
        var message = Utils.format[args[1]].parse(input),
            passphrase = Utils.format[args[0].option].parse(args[0].string),
            drop = args[3],
            encrypted = CryptoJS.RC4Drop.encrypt(message, passphrase, { drop: drop });
            
        return encrypted.ciphertext.toString(Utils.format[args[2]]);
    },
    
};


/**
 * Overwriting the CryptoJS OpenSSL key derivation function so that it is possible to not pass a
 * salt in.
 
 * @param {string} password - The password to derive from.
 * @param {number} keySize - The size in words of the key to generate.
 * @param {number} ivSize - The size in words of the IV to generate.
 * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be
 *                 generated randomly. If set to false, no salt will be added.
 *
 * @returns {CipherParams} A cipher params object with the key, IV, and salt.
 *
 * @static
 *
 * @example
 * // Randomly generates a salt
 * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32);
 * // Uses the salt 'saltsalt'
 * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt');
 * // Does not use a salt
 * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, false);
 */
CryptoJS.kdf.OpenSSL.execute = function (password, keySize, ivSize, salt) {
    // Generate random salt if no salt specified and not set to false
    // This line changed from `if (!salt) {` to the following
    if (salt === undefined || salt === null) {
        salt = CryptoJS.lib.WordArray.random(64/8);
    }

    // Derive key and IV
    var key = CryptoJS.algo.EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);

    // Separate key and IV
    var iv = CryptoJS.lib.WordArray.create(key.words.slice(keySize), ivSize * 4);
    key.sigBytes = keySize * 4;

    // Return params
    return CryptoJS.lib.CipherParams.create({ key: key, iv: iv, salt: salt });
};
